/**
 * Copyright (C) 2009 Noel Ang
 *
 * This file is part of Samsara.
 *
 * Samsara is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Samsara is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Samsara.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/
package com.lunarshades.samsara.ui;

import java.awt.Component;
import java.awt.Font;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.swing.DefaultCellEditor;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;

import com.lunarshades.samsara.Karma;
import com.lunarshades.samsara.Raider;
import com.lunarshades.samsara.ui.fonts.Fonts;

/**
 * Swing data model for a Roster object.
 *
 * @author Noel Ang sparta.variable@gmail.com
 */
public class RosterTableModel<E extends Raider> extends AbstractTableModel {

public static <E extends Raider> RosterTableModel<E> createModel(JTable table,
                                                                 Class<E> clazz
) {
    return new RosterTableModel<E>(new ArrayList<E>(), clazz, table);
}

private RosterTableModel(List<E> data, Class<E> clazz, JTable table) {
    assert data != null;
    assert clazz != null;
    assert table != null;

    mData = data;
    findColumns(clazz);
    adapt(table);
}

public SortedSet<E> sortedSet() {
    return new TreeSet<E>(mData);
}

@Override
public Class<?> getColumnClass(int columnIndex) {
    Class columnClass = null;
    synchronized (mColumns) {
        if (columnIndex >= 0 && columnIndex < mColumns.size()) {
            Class clazz = mColumns.get(columnIndex).Clazz;
            columnClass = clazz.isPrimitive() ? classMap.get(clazz) : clazz;
        }
    }
    return (columnClass == null ? Object.class : columnClass);
}

public int getRowCount() {
    synchronized (mData) {
        return mData.size();
    }
}

public int getColumnCount() {
    synchronized (mColumns) {
        return mColumns.size();
    }
}

@Override
public String getColumnName(int column) {
    synchronized (mColumns) {
        return (column < mColumns.size() ? mColumns.get(column).Name : "");
    }
}

/**
 * Set a value for a row in the model.  If the indicated row already exists, the
 * value at that row is replaced. A <code>null</code> value causes the value
 * already at the specified coordinates, to be removed.
 *
 * @param value    The value to copy to the model
 * @param rowIndex The model row coordinate for the value
 *
 * @throws IndexOutOfBoundsException  if rowIndex >= the number of rows, when
 *                                    value is null; or rowIndex > the number of
 *                                    rows, when value is not null.
 * @throws IdentityCollisionException if value.equals(n) is true for some
 *                                    existing element n in the model.
 */
public void setRowAt(E value, int rowIndex) {
    if (value == null) {
        remove(rowIndex);
    } else {
        add(rowIndex, value);
    }
}

/**
 * Get the value of a row in the model.
 *
 * @param rowIndex The model row coordinate for the value
 *
 * @return The row data, or <code>null</code> if <code>rowIndex</code> is
 *         invalid.
 */
public E getRowAt(int rowIndex) {
    E raider = null;
    synchronized (mData) {
        if (rowIndex < mData.size()) {
            raider = mData.get(rowIndex);
        }
    }
    return raider;
}

/**
 * Set a value for a data point in the model.
 * <p/>
 * If any of the arguments to the method call are invalid, the call completes
 * quietly without updating the model.
 * <p/>
 * A <code>null</code> value is not invalid; it causes the value already in the
 * model, at the specified coordinates, to be removed.
 *
 * @param value       The value to copy to the model
 * @param rowIndex    The model row coordinate for the value
 * @param columnIndex The model column coorindate for the value
 */
@Override
public void setValueAt(Object value, int rowIndex, int columnIndex) {

    Raider raider = null;

    synchronized (mColumns) {
        synchronized (mData) {
            if (rowIndex < mData.size() && columnIndex < mColumns.size()) {
                raider = mData.get(rowIndex);
            }
        }

        if (raider != null) {
            // Process the value reflectively
            ColumnInfo info = mColumns.get(columnIndex);
            Method valueSetter = info.Setter;
            Method valueGetter = info.Getter;
            Class<?> parameterType = valueGetter.getReturnType();
            if (valueSetter != null) {
                // If the runtime type of the value is incompatible with the
                // setter for this data type in the model, ask the data type to
                // resolve the type difference via {data type}.valueOf(Object),
                // if such a static method exists. Otherwise, we fail silently
                // (and the model isn't updated).
                //
                // Special case: if a primitive data type is wanted, and the
                // value's type is the wrapper equivalent of that primitive type,
                // then permit it to be used and rely on auto-(un)boxing to
                // resolve them.
                if (!parameterType.isPrimitive()
                        || classMap.get(parameterType) != value.getClass()) {
                    if (!parameterType.isInstance(value)) {
                        Method converter;
                        try {
                            converter = parameterType.getMethod("valueOf", Object.class
                            );
                            value = converter.invoke(null, value);
                        } catch (Exception e) {
                            e.printStackTrace();
                            value = null;
                        }
                    }
                }
                if (value != null) {
                    try {
                        valueSetter.invoke(raider, value);
                        fireTableCellUpdated(rowIndex, columnIndex);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

/**
 * Get the value for a data point in the model.
 *
 * @param rowIndex    The model row coordinate for the value
 * @param columnIndex The model column coorindate for the value
 *
 * @return The value at the table model coordinates given or <code>null</code>
 *         if the coordinates are invalid.
 */
public Object getValueAt(int rowIndex, int columnIndex) {
    Object value = null;
    Raider raider = null;
    Method valueGetter = null;
    synchronized (mColumns) {
        synchronized (mData) {
            if (rowIndex < mData.size()) {
                raider = mData.get(rowIndex);
            }
        }
        if (raider != null) {
            if (columnIndex < mColumns.size()) {
                valueGetter = mColumns.get(columnIndex).Getter;
            }
            if (valueGetter != null) {
                try {
                    value = valueGetter.invoke(raider);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    return value;
}

private void add(int rowIndex, E raider) {
    boolean isNewRow = false;

    synchronized (mData) {
        int size = mData.size();
        if (rowIndex < size) {
            // Replace row
            E aRaider = mData.remove(rowIndex);
            if (!mData.contains(raider)) {
                mData.add(rowIndex, raider);
            } else {
                mData.add(rowIndex, aRaider);
                throw new IdentityCollisionException(raider.name());
            }
        } else if (rowIndex == size) {
            // Add row
            if (!mData.contains(raider)) {
                mData.add(raider);
            } else {
                throw new IdentityCollisionException(raider.name());
            }
            isNewRow = true;
        } else {
            throw new IndexOutOfBoundsException("rowIndex: "
                    + rowIndex
                    + " size: "
                    + mData.size()
            );
        }
    }
    if (isNewRow) {
        fireTableRowsInserted(rowIndex, rowIndex);
    } else {
        fireTableRowsUpdated(rowIndex, rowIndex);
    }
}

private void remove(int rowIndex) {
    boolean removed;

    synchronized (mData) {
        if (rowIndex < mData.size()) {
            mData.remove(rowIndex);
            removed = true;
        } else {
            throw new IndexOutOfBoundsException("rowIndex: "
                    + rowIndex
                    + " size: "
                    + mData.size()
            );
        }
    }
    if (removed) {
        fireTableRowsDeleted(rowIndex, rowIndex);
    }
}

@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
    synchronized (mColumns) {
        if (columnIndex < mColumns.size()) {
            ColumnInfo info = mColumns.get(columnIndex);
            return info.isEditable;
        }
        return false;
    }
}

public final void adapt(JTable table) {
    assert table != null;
    table.setModel(this);
    table.setRowSorter(new RosterTableRowSorter<RosterTableModel>(this, 0));
    table.setAutoCreateRowSorter(false);
    registerCustomEditors(table);
    registerCustomRenderers(table);
    registerCustomHeaders(table);
}

public int getVisibleRowCount() {
    int count = 0;
    synchronized (mData) {
        for (E elem : mData) {
            if (elem.isIncluded()) {
                count++;
            }
        }
    }
    return count;
}

private void registerCustomEditors(JTable table) {
    table.setDefaultEditor(Karma.class, new DefaultCellEditor(new JTextField())
    );
}

private void registerCustomRenderers(JTable table) {
    table.setDefaultRenderer(Karma.class, new DefaultTableCellRenderer() {
        @Override
        public Component getTableCellRendererComponent(JTable table,
                                                       Object value,
                                                       boolean isSelected,
                                                       boolean hasFocus,
                                                       int row,
                                                       int column
        ) {
            Component component = super.getTableCellRendererComponent(table,
                    value,
                    isSelected,
                    hasFocus,
                    row,
                    column
            );
            if (Karma.class.isInstance(value)) {
                setHorizontalAlignment(RIGHT);
                Font currentFont = getFont();
                if (!currentFont.getFamily().equals(NUMBER_FONT.getFamily())) {
                    component.setFont(
                            NUMBER_FONT.deriveFont(currentFont.getStyle()));
                }
            }
            return component;
        }
    }
    );
}

private void registerCustomHeaders(JTable table) {
    JTableHeader header = table.getTableHeader();
    TableColumnModel model = header.getColumnModel();
    Enumeration<TableColumn> columns = model.getColumns();
    while (columns.hasMoreElements()) {
        TableColumn c = columns.nextElement();
        if ("Karma".equals(String.valueOf(c.getHeaderValue()))) {
            c.setResizable(false);
            c.setMaxWidth(c.getPreferredWidth());
            c.setMinWidth(c.getPreferredWidth());
        } else if ("Pickup".equals(String.valueOf(c.getHeaderValue()))) {
            c.setResizable(false);
            c.setMaxWidth(c.getPreferredWidth());
            c.setMinWidth(c.getPreferredWidth());
            c.setHeaderValue("Pickup?");
        }
    }
}

/**
 * Collect column metadata.
 * <p/>
 * The subject type of this data model (Raider) is examined through reflection.
 * Methods annotated {@link TableColumn} are taken as columns for the model.
 * <p/>
 * For a given method whose name is N, whose return type is R, that is annotated
 * as such, a search is made for another method whose name is "set" + N and that
 * declares a single formal parameter of type R.  If that method exists, the
 * column is marked editable, and the latter method is remembered and later used
 * to mutate the model data at its designated column.
 *
 * @param clazz The runtime class to inspect, whose annotated members define the
 *              column model
 *
 * @see com.lunarshades.samsara.ui.TableColumn
 */
private void findColumns(Class<?> clazz) {
    assert clazz != null;

    Method[] methods = clazz.getMethods();

    // Declared methods take precedence over inherited methods, as column order,
    // so we build two separate lists and merge them later.
    SortedSet<ColumnInfo> priorityColumns = new TreeSet<ColumnInfo>();
    SortedSet<ColumnInfo> remainderColumns = new TreeSet<ColumnInfo>();
    for (Method m : methods) {
        if (m.isAnnotationPresent(com.lunarshades.samsara.ui.TableColumn.class)) {
            boolean priority = m.getDeclaringClass().equals(clazz);
            SortedSet<ColumnInfo> destinationList =
                    (priority ? priorityColumns : remainderColumns);
            ColumnInfo info = new ColumnInfo();
            com.lunarshades.samsara.ui.TableColumn tableColumn =
                    m.getAnnotation(com.lunarshades.samsara.ui.TableColumn.class)
                    ;
            info.Name = Utils.toWords(m.getName());
            info.Clazz = m.getReturnType();
            info.Getter = m;
            info.Setter = findSetterForGetter(methods, m);
            info.isEditable = (info.Setter != null);
            info.order = tableColumn.value();
            destinationList.add(info);
        }
    }
    synchronized (mColumns) {
        mColumns.clear();
        mColumns.addAll(priorityColumns);
        mColumns.addAll(remainderColumns);
    }
    priorityColumns.clear();
    remainderColumns.clear();
}

private Method findSetterForGetter(Method[] methods, Method m) {
    // Setter to look for is "set" + {name of the m in title case}
    String targetName = new StringBuilder(Utils.capitalize(m.getName())).insert(
            0,
            "set"
    ).toString().intern();
    Class<?> targetParameterType = m.getReturnType();
    Method setter = null;

    for (Method method : methods) {
        String name = method.getName();
        if (name.equals(targetName)) {
            // Candidate setter must have a single parameter of the same
            // type as or subclass of the getter return type
            Class<?>[] parameterTypes = method.getParameterTypes();
            if (parameterTypes.length == 1
                    && targetParameterType.isAssignableFrom(parameterTypes[0])) {
                setter = method;
                break;
            }
        }
    }
    return setter;
}

/**
 * Internal structure column metadata
 */
class ColumnInfo implements Comparable {

    public int compareTo(Object o) {
        ColumnInfo other = (ColumnInfo) o;
        int comparison;
        if (this == other) {
            comparison = 0;
        } else {
            if (other == null) {
                comparison = 1;
            } else if (order != other.order) {
                comparison = order - other.order;
            } else {
                comparison = Name.compareTo(other.Name);
            }
        }
        return comparison;
    }

    volatile String Name;
    volatile Class Clazz;
    volatile Method Getter;
    volatile Method Setter;
    volatile boolean isEditable;
    volatile int order;
}

public static class IdentityCollisionException extends RuntimeException {
    private IdentityCollisionException(String message) {
        super(message);
    }
}

static Map<Class, Class> classMap = new HashMap<Class, Class>(8);
static {
    classMap.put(boolean.class, Boolean.class);
    classMap.put(char.class, Character.class);
    classMap.put(int.class, Integer.class);
    classMap.put(double.class, Double.class);
    classMap.put(float.class, Float.class);
    classMap.put(long.class, Long.class);
    classMap.put(void.class, Void.class);
    classMap.put(byte.class, Byte.class);
}

// Principal structure, this is the object that is persisted
private final List<E> mData;

// Column metadata
private final List<ColumnInfo> mColumns = new ArrayList<ColumnInfo>(10);

private static final Font NUMBER_FONT = Fonts.getNumberFont();
}
